/*
 ------------------------------------------------------------------------------
 Copyright (C) 2009-2011 Eternal Games.

 This file is part of the Eternal Tech source code.

 The Eternal Tech source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The Eternal Tech source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the Eternal Tech source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// r_shader.cpp - Shader script parsing and program linking
//


#include "r_local.h"


struct uniformTable_t {
	const char				*name;

	uniformType_t			type;
	int						size;
	uint					format;
};

static uniformTable_t	r_uniformTable[] = {
	{"u_ViewOrigin",		UT_VIEW_ORIGIN,				1,	GL_FLOAT_VEC3},
	{"u_ViewAxis",			UT_VIEW_AXIS,				1,	GL_FLOAT_MAT3},
	{"u_EntityOrigin",		UT_ENTITY_ORIGIN,			1,	GL_FLOAT_VEC3},
	{"u_EntityAxis",		UT_ENTITY_AXIS,				1,	GL_FLOAT_MAT3},
	{"u_CoordScaleAndBias",	UT_COORD_SCALE_AND_BIAS,	1,	GL_FLOAT_VEC4},
	{"u_ColorScaleAndBias",	UT_COLOR_SCALE_AND_BIAS,	1,	GL_FLOAT_VEC2},
	{NULL,					UT_CUSTOM,					0,	0}
};

static shader_t				*r_shadersHashTable[SHADERS_HASH_SIZE];
static shader_t				*r_shaders[MAX_SHADERS];
static int					r_numShaders;

static program_t			*r_programsHashTable[PROGRAMS_HASH_SIZE];
static program_t			*r_programs[MAX_PROGRAMS];
static int					r_numPrograms;


/*
 ==============================================================================

    SHADER REGISTRATION AND CREATION

 ==============================================================================
*/


/*
 ==================
 R_PrintShaderInfoLog
 ==================
*/
static void R_PrintShaderInfoLog (shader_t *shader)
{
	char	infoLog[MAX_PRINT_MESSAGE];
	int		infoLogLength;

	qglGetObjectParameterivARB(shader->shaderId, GL_INFO_LOG_LENGTH, &infoLogLength);
	if (infoLogLength <= 0)
		return;

	qglGetInfoLogARB(shader->shaderId, sizeof(infoLog), NULL, infoLog);

	Com_DevPrintf("---------- Shader Info Log ----------\n");
	Com_DevPrintf("%s", infoLog);

	if (infoLogLength >= sizeof(infoLog))
		Com_DevPrintf("...\n");

	Com_DevPrintf("-------------------------------------\n");
}

/*
 ==================
 R_CompileShader
 ==================
*/
static void R_CompileShader (shader_t *shader, const char *code)
{
	switch (shader->uploadTarget){
	case GL_VERTEX_SHADER:
		Com_DevPrintf("Compiling GLSL vertex shader '%s'...\n", shader->name);
		break;
	case GL_FRAGMENT_SHADER:
		Com_DevPrintf("Compiling GLSL fragment shader '%s'...\n", shader->name);
		break;
	}

	// Create the shader
	shader->shaderId = qglCreateShaderObjectARB(shader->uploadTarget);

	// Upload and compile the shader source
	qglShaderSourceARB(shader->shaderId, 1, &code, NULL);
	qglCompileShaderARB(shader->shaderId);

	// Check for any errors
	qglGetObjectParameterivARB(shader->shaderId, GL_OBJECT_COMPILE_STATUS_ARB, &shader->compileStatus);

	if (!shader->compileStatus)
	{
		switch (shader->uploadTarget){
		case GL_VERTEX_SHADER:
			Com_Printf(S_COLOR_RED "Failed to compile vertex shader '%s'\n", shader->name);
			break;
		case GL_FRAGMENT_SHADER:
			Com_Printf(S_COLOR_RED "Failed to compile fragment shader '%s'\n", shader->name);
			break;
		}

		// If there is anything to log, print it to the console
		R_PrintShaderInfoLog(shader);

		return;
	}

	// If there is anything to log, print it to the console
	R_PrintShaderInfoLog(shader);
}

/*
 ==================
 R_LoadShader
 ==================
*/
static shader_t *R_LoadShader (const char *name, uint target, const char *code)
{
	shader_t   *shader;
	uint		hashKey;

	// Allocate a new shader
	if (r_numShaders == MAX_SHADERS)
		Com_Error(ERR_DROP, "R_LoadShader: MAX_SHADER hit");

	r_shaders[r_numShaders++] = shader = (shader_t *)Mem_Alloc(sizeof(shader_t), rg.shaderMemoryPool);

	// Fill it in
	Str_Copy(shader->name, name, sizeof(shader->name));
	shader->uploadTarget = target;

	// Create it
	R_CompileShader(shader, code);

	// Add to hash table
	hashKey = Str_HashKey(shader->name, SHADERS_HASH_SIZE, false);

	shader->nextHash = r_shadersHashTable[hashKey];
	r_shadersHashTable[hashKey] = shader;

	return shader;
}

/*
 ==================
 R_FindShader
 ==================
*/
shader_t *R_FindShader (const char *name, uint target)
{
	shader_t   *shader;
	char		realName[MAX_PATH_LENGTH];
	char		*code;
	uint		hashKey;

	if (!name || !name[0])
		Com_Error(ERR_DROP, "R_FindShader: NULL shader name");

	if (Str_Length(name) >= MAX_PATH_LENGTH)
		Com_Error(ERR_DROP, "R_FindShader: shader name exceeds MAX_PATH_LENGTH");

	// Check if it already exists
	hashKey = Str_HashKey(name, SHADERS_HASH_SIZE, false);

	for (shader = r_shadersHashTable[hashKey]; shader; shader = shader->nextHash)
	{
		if (shader->uploadTarget != target)
			continue;

		if (!Str_ICompare(shader->name, name))
		{
			if (!shader->compileStatus)
				return NULL;

			return shader;
		}
	}

	// Load the script code
	switch (target){
	case GL_VERTEX_SHADER:
		Str_SPrintf(realName, sizeof(realName), "shaders/%s.vs", name);
		break;
	case GL_FRAGMENT_SHADER:
		Str_SPrintf(realName, sizeof(realName), "shaders/%s.fs", name);
		break;
	default:
		Com_Error(ERR_FATAL, "R_FindShader: bad shader type (%u)", target);
	}

	FS_ReadFile(realName, (void **)&code);
	if (!code)
		return NULL;

	// Load it in
	shader = R_LoadShader(name, target, code);

	// Free file data
	FS_FreeFile2(code);

	if (!shader->compileStatus)
		return NULL;

	return shader;
}

/*
 ==================
 R_RegisterShader
 ==================
*/
shader_t *R_RegisterShader (const char *name, uint target)
{
	return R_FindShader(name, target);
}


/*
 ==============================================================================

    UNIFORM AND SAMPLER FUNCTIONS

 ==============================================================================
*/


/*
 ==================
 R_GetProgramUniform
 ==================
*/
uniform_t *R_GetProgramUniform (program_t *program, const char *name)
{
	uniform_t   *uniform;
	int			i;

	for (i = 0, uniform = program->uniforms; i < program->numUniforms; i++, uniform++)
	{
		if (!Str_Compare(uniform->name, name))
			return uniform;
	}

	return NULL;
}

/*
 ==================
 R_SetProgramSampler
 ==================
*/
void R_SetProgramSampler (program_t *program, uniform_t *uniform, int unit)
{
	if (uniform->unit == unit)
		return;
	uniform->unit = unit;

	GL_BindProgram(program);
	qglUniform1iARB(uniform->location, uniform->unit);
	GL_UnbindPrograms();
}

/*
 ==================
 R_UniformFloat
 ==================
*/
void R_UniformFloat (uniform_t *uniform, float v0)
{
	if (uniform->values[0] == v0)
		return;

	uniform->values[0] = v0;

	qglUniform1fARB(uniform->location, v0);
}

/*
 ==================
 R_UniformFloat2
 ==================
*/
void R_UniformFloat2 (uniform_t *uniform, float v0, float v1)
{
	if (uniform->values[0] == v0 && uniform->values[1] == v1)
		return;

	uniform->values[0] = v0;
	uniform->values[1] = v1;

	qglUniform2fARB(uniform->location, v0, v1);
}

/*
 ==================
 R_UniformFloat3
 ==================
*/
void R_UniformFloat3 (uniform_t *uniform, float v0, float v1, float v2)
{
	if (uniform->values[0] == v0 && uniform->values[1] == v1 && uniform->values[2] == v2)
		return;

	uniform->values[0] = v0;
	uniform->values[1] = v1;
	uniform->values[2] = v2;

	qglUniform3fARB(uniform->location, v0, v1, v2);
}

/*
 ==================
 R_UniformFloat4
 ==================
*/
void R_UniformFloat4 (uniform_t *uniform, float v0, float v1, float v2, float v3)
{
	if (uniform->values[0] == v0 && uniform->values[1] == v1 && uniform->values[2] == v2 && uniform->values[3] == v3)
		return;

	uniform->values[0] = v0;
	uniform->values[1] = v1;
	uniform->values[2] = v2;
	uniform->values[3] = v3;

	qglUniform4fARB(uniform->location, v0, v1, v2, v3);
}

/*
 ==================
 R_UniformVector3
 ==================
*/
void R_UniformVector3 (uniform_t *uniform, const vec3_t v)
{
	if (uniform->values[0] == v[0] && uniform->values[1] == v[1] && uniform->values[2] == v[2])
		return;

	uniform->values[0] = v[0];
	uniform->values[1] = v[1];
	uniform->values[2] = v[2];

	qglUniform3fvARB(uniform->location, 1, v);
}

/*
 ==================
 R_UniformMatrix3
 ==================
*/
void R_UniformMatrix3 (uniform_t *uniform, bool transpose, const mat3_t m)
{
	uniform->matrixIdentity = false;

	qglUniformMatrix3fvARB(uniform->location, 1, transpose, (float *)m);
}


/*
 ==============================================================================

    SHADER PROGRAM UNIFORM PARSING

 ==============================================================================
*/


/*
 ==================
 R_ParseProgramUniforms
 ==================
*/
static void R_ParseProgramUniforms (program_t *program)
{
	uniformTable_t   *uniformTable;
	uniform_t		 *uniform, uniforms[MAX_PROGRAM_UNIFORMS];
	int				 numUniforms;
	char			 name[MAX_UNIFORM_NAME_LENGTH];
	uint			 format;
	int				 length, size;
	int				 location;
	int				 i;

	// Get active uniforms
	qglGetObjectParameterivARB(program->programId, GL_ACTIVE_UNIFORMS, &numUniforms);
	if (!numUniforms)
		return;

	for (i = 0; i < numUniforms; i++)
	{
		qglGetActiveUniformARB(program->programId, i, sizeof(name), &length, &size, &format, name);

		location = qglGetUniformLocationARB(program->programId, name);

		if (location == -1)
			continue;   // Ignore built-in uniforms

		// Fix up uniform array names
		if (name[length-3] == '[' && name[length-2] == '0' && name[length-1] == ']')
			name[length-3] = 0;

		// Check for a predefined uniform name
		for (uniformTable = r_uniformTable; uniformTable->name; uniformTable++)
		{
			if (!Str_Compare(uniformTable->name, name))
				break;
		}

		if (uniformTable->type != UT_CUSTOM)
		{
			if (uniformTable->size != size)
				Com_Error(ERR_DROP, "R_ParseProgramUniforms: uniform '%s' in program '%s' has wrong size", name, program->name);

			if (uniformTable->format != format)
				Com_Error(ERR_DROP, "R_ParseProgramUniforms: uniform '%s' in program '%s' has wrong format", name, program->name);
		}

		// Allocate a new uniform
		if (program->numUniforms == MAX_PROGRAM_UNIFORMS)
			Com_Error(ERR_DROP, "R_ParseProgramUniforms: MAX_PROGRAM_UNIFORMS hit");

		uniform = &uniforms[program->numUniforms++];

		Str_Copy(uniform->name, name, sizeof(uniform->name));
		uniform->type = uniformTable->type;
		uniform->size = size;
		uniform->format = format;
		uniform->location = location;
		uniform->unit = -1;
		uniform->values[0] = 0.0f;
		uniform->values[1] = 0.0f;
		uniform->values[2] = 0.0f;
		uniform->values[3] = 0.0f;
		uniform->matrixIdentity = false;
	}

	// Copy the uniforms
	program->uniforms = (uniform_t *)Mem_DupData(uniforms, program->numUniforms * sizeof(uniform_t), rg.shaderMemoryPool);
}


/*
 ==============================================================================

    SHADER PROGRAM REGISTRATION AND CREATION

 ==============================================================================
*/


/*
 ==================
 R_PrintProgramInfoLog
 ==================
*/
static void R_PrintProgramInfoLog (program_t *program)
{
	char	infoLog[MAX_PRINT_MESSAGE];
	int		infoLogLength;

	qglGetObjectParameterivARB(program->programId, GL_INFO_LOG_LENGTH, &infoLogLength);
	if (infoLogLength <= 0)
		return;

	qglGetInfoLogARB(program->programId, sizeof(infoLog), NULL, infoLog);

	Com_DevPrintf("---------- Program Info Log ----------\n");
	Com_DevPrintf("%s", infoLog);

	if (infoLogLength >= sizeof(infoLog))
		Com_DevPrintf("...\n");

	Com_DevPrintf("--------------------------------------\n");
}

/*
 ==================
 R_LinkProgram
 ==================
*/
static void R_LinkProgram (program_t *program)
{
	// Create the program
	program->programId = qglCreateProgramObjectARB();

	// Attach the shaders
	qglAttachObjectARB(program->programId, program->vertexShader->shaderId);
	qglAttachObjectARB(program->programId, program->fragmentShader->shaderId);

	// TODO: Add more vertex attributes

	// Bind vertex attributes
	qglBindAttribLocationARB(program->programId, 10, "va_Tangent1");
	qglBindAttribLocationARB(program->programId, 11, "va_Tangent2");

	// Link the program
	qglLinkProgramARB(program->programId);

	// Check for any errors
	qglGetObjectParameterivARB(program->programId, GL_LINK_STATUS, &program->linkStatus);

	if (!program->linkStatus)
	{
		Com_Printf(S_COLOR_RED "Failed to link program '%s'\n", program->name);

		// If there is anything to log, print it to the console
		R_PrintProgramInfoLog(program);

		return;
	}

	// If there is anything to log, print it to the console
	R_PrintProgramInfoLog(program);

	// TODO: Parse the active vertex attributes

	// Parse the active uniforms
	R_ParseProgramUniforms(program);
}

/*
 ==================
 R_LoadProgram
 ==================
*/
static program_t *R_LoadProgram (const char *name, shader_t *vertexShader, shader_t *fragmentShader)
{
	program_t   *program;
	uint		hashKey;

	// Allocate a new program
	if (r_numPrograms == MAX_PROGRAMS)
		Com_Error(ERR_DROP, "R_LoadProgram: MAX_PROGRAMS hit");

	r_programs[r_numPrograms++] = program = (program_t *)Mem_Alloc(sizeof(program_t), rg.shaderMemoryPool);

	// Fill it in
	Str_Copy(program->name, name, sizeof(program->name));
	program->vertexShader = vertexShader;
	program->fragmentShader = fragmentShader;
	program->numUniforms = 0;
	program->uniforms = NULL;

	// Create it
	R_LinkProgram(program);

	// Add to hash table
	hashKey = Str_HashKey(program->name, PROGRAMS_HASH_SIZE, false);

	program->nextHash = r_programsHashTable[hashKey];
	r_programsHashTable[hashKey] = program;

	return program;
}

/*
 ==================
 R_FindProgram
 ==================
*/
program_t *R_FindProgram (const char *name, shader_t *vertexShader, shader_t *fragmentShader)
{
	program_t   *program;
	uint		hashKey;

	if (!name || !name[0])
		Com_Error(ERR_DROP, "R_FindProgram: NULL shader program name");

	if (Str_Length(name) >= MAX_PATH_LENGTH)
		Com_Error(ERR_DROP, "R_FindProgram: shader program name exceeds MAX_PATH_LENGTH");

	// Check if it already exists
	hashKey = Str_HashKey(name, PROGRAMS_HASH_SIZE, false);

	for (program = r_programsHashTable[hashKey]; program; program = program->nextHash)
	{
		if (program->vertexShader != vertexShader || program->fragmentShader != fragmentShader)
			continue;

		if (!Str_ICompare(program->name, name))
		{
			if (!program->linkStatus)
				return NULL;

			return program;
		}
	}

	// Load it in
	program = R_LoadProgram(name, vertexShader, fragmentShader);

	if (!program->linkStatus)
		return NULL;

	return program;
}


/*
 ==============================================================================

    CONSOLE COMMANDS

 ==============================================================================
*/


/*
 ==================
 R_ListShaders_f
 ==================
*/
void R_ListShaders_f ()
{
	shader_t   *shader;
	int		   i;

	Com_Printf("\n");
	Com_Printf("      type -name-----------\n");

	for (i = 0; i < r_numShaders; i++)
	{
		shader = r_shaders[i];

		Com_Printf("%4i: ", i);

		switch (shader->uploadTarget){
		case GL_VERTEX_SHADER:
			Com_Printf(" VS  ");
			break;
		case GL_FRAGMENT_SHADER:
			Com_Printf(" FS  ");
			break;
		default:
			Com_Printf("???? ");
			break;
		}

		Com_Printf("%s%s\n", shader->name, (!shader->compileStatus) ? " (INVALID)" : "");
	}

	Com_Printf("---------------------------\n");
	Com_Printf("%i total shaders\n", r_numShaders);
	Com_Printf("\n");
}

/*
 ==================
 R_ListPrograms_f
 ==================
*/
void R_ListPrograms_f ()
{
	program_t	*program;
	int			i;

	Com_Printf("\n");
	Com_Printf("      ufrms -name-----------\n");

	for (i = 0; i < r_numPrograms; i++)
	{
		program = r_programs[i];

		Com_Printf("%4i: ", i);

		Com_Printf("%5i ", program->numUniforms);

		Com_Printf("%s%s\n", program->name, (!program->linkStatus) ? " (INVALID)" : "");
	}

	Com_Printf("----------------------------\n");
	Com_Printf("%i total programs\n", r_numPrograms);
	Com_Printf("\n");
}


/*
 ==============================================================================

    INITIALIZATION AND SHUTDOWN

 ==============================================================================
*/


/*
 ==================
 R_InitShaders
 ==================
*/
void R_InitShaders ()
{
	shader_t   *vertexShader, *fragmentShader;

	// Load ambient shaders
	vertexShader = R_FindShader("ambient", GL_VERTEX_SHADER);
	fragmentShader = R_FindShader("ambient", GL_FRAGMENT_SHADER);

	rg.ambientProgram = R_FindProgram("ambient", vertexShader, fragmentShader);

	// Load interaction shaders
	vertexShader = R_FindShader("interaction", GL_VERTEX_SHADER);
	fragmentShader = R_FindShader("interaction", GL_FRAGMENT_SHADER);

	rg.interactionProgram = R_FindProgram("interaction", vertexShader, fragmentShader);
}

/*
 ==================
 R_ShutdownShaders
 ==================
*/
void R_ShutdownShaders ()
{
	program_t   *program;
	shader_t    *shader;
	int		    i;

	// Delete all the programs
	qglUseProgramObjectARB(0);

	for (i = 0; i < r_numPrograms; i++)
	{
		program = r_programs[i];

		qglDetachObjectARB(program->programId, program->vertexShader->shaderId);
		qglDetachObjectARB(program->programId, program->fragmentShader->shaderId);

		qglDeleteObjectARB(program->programId);
	}

	// Clear program list
	memset(r_programsHashTable, 0, sizeof(r_programsHashTable));
	memset(r_programs, 0, sizeof(r_programs));

	r_numPrograms = 0;

	// Delete all the shaders
	for (i = 0; i < r_numShaders; i++)
	{
		shader = r_shaders[i];

		qglDeleteObjectARB(shader->shaderId);

		Mem_Free(shader);
	}

	// Clear shader list
	memset(r_shadersHashTable, 0, sizeof(r_shadersHashTable));
	memset(r_shaders, 0, sizeof(r_shaders));

	r_numShaders = 0;
}